[要移行] AWS WAF Classic (v1)が2025年9月30日で終了します

[要移行] AWS WAF Classic (v1)が2025年9月30日で終了します

Clock Icon2024.09.28

しばたです。

前の記事は既に更新済みですが、2024年9月26日ごろから利用者向けにAWS WAF Classic (v1)のサービス終了が通知され始めています。

https://dev.classmethod.jp/articles/summary-of-aws-service-eos-announcement-at-202409/

本記事では通知の詳細と既存リソースの移行方法を簡単に解説します。

詳細

通知の詳細は各自でご確認頂きたいのですが、最初の一文だけ引用すると

We are contacting you because your AWS account has AWS WAF Classic resources. After careful consideration, we have decided to end support for AWS WAF Classic. Starting March 2025, you will not be able to create new WAF Classic WebACLs. On September 30, 2025, we will retire AWS WAF Classic, meaning you must migrate your AWS resources from WAF Classic to AWS WAF before this date if you want to continue to have AWS WAF protections.

とあり、

  • 2025年3月より新規にClassic ACLを作成することは不可
  • 2025年9月30日にAWS WAF Classic (v1)のサービス終了

というスケジュールになっています。

2025年9月30日以後はサービスが利用できなくなるため、AWS WAF Classic (v1)の利用者は現行バージョンであるAWS WAF v2にリソースを移行する必要があります。

移行方法

AWS WAF Classic (v1)とAWS WAF v2ではリソースに互換性が無いため、移行には

  1. AWS WAF v2に新規リソース(ACL)を作成する
  2. 新規ACLの設定をAWS WAF Classicと同等にする
  3. 保護対象リソース(ALBなど)との関連付けをAWS WAF v2 ACLに切り替える

という手順を踏む必要があります。

一応AWSからは既存AWS Classic WAF ACLの設定を読み込みAWS WAF v2 ACL用のCloudFormationテンプレートを生成する移行ツールが提供されているのですが、このツールは制限事項が多く[1]、あくまでも前述の1.と2.の作業を補助するためのものと考えるのが現実的です。

移行ツールを使った場合でも手作業による設定内容の確認や追加設定が必要になると思います。
このため移行作業は余裕をもって計画的に行うことをお勧めします。

移行ツールについて

移行ツールはマネジメントコンソール上でウィザード形式で処理を進める形で提供され、最終的にユーザー指定のS3バケット上にCloudFormationテンプレートのJSONファイルを出力します。
出力されたテンプレートファイルを使ってCloudFormationスタックを作成しAWS WAF v2のリソースを作成します。

ツールの詳細については以下のドキュメントをご確認ください。

前提条件

移行ツールを実行する前にJSONファイル保存用のS3バケットを用意する必要があります。

S3バケットはaws-waf-migration-で始まる名称である必要があり、暗号化方式はSSE-S3のみサポートされています。
そしてS3バケットのリージョンは移行するリソースがあるリージョンと同一にする必要があります。

加えてAWS WAFのサービスからアクセス可能にするためのバケットポリシーを設定する必要もあるのですが、こちらについては移行ツールのウィザードから自動設定することも可能です。

制限事項

移行ツールの制限事項については以下のドキュメントに記載されています。

このドキュメントの内容をかいつまんで解説していきます。

制限1 : 移行ツールは単一アカウント内でのみ利用可能

ドキュメントでは

You can only migrate AWS WAF Classic resources for any account to AWS WAF resources for the same account.

と記載されており、同一アカウント内でのリソース移行のみ可能とされています。

ただ、移行ツールはあくまでもCloudFormationテンプレートの生成まで行うだけであり、テンプレートを別AWSアカウントで実行することは出来てしまうでしょう。
このためこの制限は「別アカウントへの移行は想定外でありサポート外」という意図だと思います。

制限2 : AWS Marketplaceサブスクリプションによるルールは移行対象外

AWS WAF ClassicはまだAWS管理のマネージドルールグループが無い時代のWAFであり、基本的に

  • 利用者が自分で設定するルールおよびルールグループ
  • Marketplaceからセキュリティベンダーの提供するルールをサブスクライブする

の2パターンでルール設定できますが、このうちMarketplaceのサブスクリプションはツールで移行できません。

これは単純にMarketplace製品にバージョン間の互換が無いためです。
MarketplaceサブスクリプションについてはAWS WAF v2 ACLを作成した後に手動で同等製品をサブスクライブしなおす必要があります。

たとえば、WafCharmを提供しているCyber Security Cloud社の製品だと

といった感じでAWS WAFのバージョン毎に製品が分かれています。

私が調べた限りだと必ず互換製品があるわけでは無い様なので、場合によっては「AWS WAF Classicとは全く異なる製品をサブスクライブする」か「AWSマネージドのルールグループに切り替える」必要があるでしょう。

制限3 : ACLと保護対象リソースの関連付けは移行されない

移行ツールが出力するCloudFormationテンプレートではACLと保護対象リソースの関連付けは移行されません。

これは移行前の検証や利用者自身のタイミングでの切り替えを想定した意図的な制限となります。

制限4 : ログ設定はデフォルト無効となる

移行ツールが出力するCloudFormationテンプレートは必ずログ出力設定が無効の状態となります。
こちらについては

This is by design, to avoid affecting your production workload.

という説明がされていますが、実態としてはAWS WAF ClassicとAWS WAF v2でログ出力の方式が結構異なる(AWS WAF v2の方が選択肢が多い)ためだと思います。

このためログ出力設定はAWS WAF v2 ACLを作った後に手作業で追加設定してやる必要があります。
(生成されるCloudFormationテンプレートを改変しても良いでしょう)

制限5 : AWS Firewall Managerにより管理されるルールグループは移行対象外

移行対象のAWS WAF Classic ACLにAWS Firewall Managerにより管理されるルールグループが含まれる場合、そのルールグループは移行されません。

AWS Firewall Managerにより管理されるルールグループはAWS Firewall Managerから再設定する必要があります。

制限6 : AWS WAF Security Automationsソリューションの移行はサポート外

AWS WAF Security Automationsソリューションにより作成された環境はこのツールのサポート外です。

このツールで移行できる対象はAWS WAF ACLだけであり、ソリューションが提供するLambda関数等のリソースを含んでいないのが理由となります。
AWSからはAWS WAF v2を使う新しいバージョンを再デプロイする様指示されています。

移行してみた

ここからは実際に移行ツールを試してみた手順と結果を共有します。

注意事項

0. 検証環境

今回は私の検証用AWSアカウントの東京リージョンにAWS WAF Classic ACLを一つ用意しました。
ACLは過去に公開された古いOWASP Top 10ルールを含むCloudFormationテンプレートから作成しました。

https://aws.amazon.com/jp/about-aws/whats-new/2017/07/use-aws-waf-to-mitigate-owasps-top-10-web-application-vulnerabilities/

これによりgeneric-owasp-aclという名前のClassic ACLと各種ルールが作成済みの状態です。
aws-waf-classic-v1-eos-and-migration-01

aws-waf-classic-v1-eos-and-migration-02
このACLには10個の個別ルールが設定済み

加えてテスト用のALB(test-alb)を用意し保護対象として関連付けています。

aws-waf-classic-v1-eos-and-migration-03

また、CloudFormationテンプレートの出力先としてaws-waf-migration-test-20240928という名前のS3バケットを作成済みです。

aws-waf-classic-v1-eos-and-migration-04

1. 移行ツールの実行

マネジメントコンソールからAWS WAF Classicにアクセスすると画面上部にサポート終了の警告が表示される様になっています。

aws-waf-classic-v1-eos-and-migration-05

この警告文に移行ツール(migration wizard)へのリンクがあるのでクリックします。

aws-waf-classic-v1-eos-and-migration-06

移行ウィザードが開始されるので最初に対象とするACLを選択します。

aws-waf-classic-v1-eos-and-migration-07

続けてCloudFormationテンプレート出力先とするS3バケットを選択します。
S3バケット名がaws-waf-migration-で始まらないと対象にリストアップされませんのでご注意ください。

aws-waf-classic-v1-eos-and-migration-08

ここでバケットポリシーを指定することができるので今回は「Auto apply the bucket policy required for migration」を選択します。
既にバケットポリシーを指定済みの場合は「Use the bucket policy that comes with the S3 bucket」を選ぶと良いでしょう。

また、移行処理について今回は「Exclude rules that can't be migrated」を選んでいます。
エラー無く移行できることを重視する場合は「Stop the migration process if a rule can't be migrated」を選んでください。

この状態で次へ進むと最終確認になるのでそのまま「Start creating CloudFormation template」をクリックします。

aws-waf-classic-v1-eos-and-migration-09

環境にもよるでしょうが、今回は一瞬で処理が完了しました。

aws-waf-classic-v1-eos-and-migration-10

S3バケットにJSON形式のテンプレートファイルが生成されて終了となります。

ちなみにJSONファイルは1行に圧縮された形になっているのでご注意ください。
一応以下に生成された内容を記載しておきます。

クリックして展開
フォーマット済みのテンプレートファイル
{
	"Resources": {
		"Stackgenericowaspacl1727502423894": {
			"Type": "AWS::WAFv2::WebACL",
			"Properties": {
				"DefaultAction": {
					"Allow": {}
				},
				"Scope": "REGIONAL",
				"VisibilityConfig": {
					"CloudWatchMetricsEnabled": true,
					"MetricName": "genericowaspacl",
					"SampledRequestsEnabled": true
				},
				"Description": "genericowaspacl",
				"Name": "generic-owasp-acl928a69a5-e49b-4237-9d23-23793211898a",
				"Rules": [
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-restrict-sizes",
						"Priority": 10,
						"Statement": {
							"OrStatement": {
								"Statements": [
									{
										"SizeConstraintStatement": {
											"ComparisonOperator": "GT",
											"FieldToMatch": {
												"UriPath": {}
											},
											"Size": 512,
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "NONE"
												}
											]
										}
									},
									{
										"SizeConstraintStatement": {
											"ComparisonOperator": "GT",
											"FieldToMatch": {
												"QueryString": {}
											},
											"Size": 1024,
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "NONE"
												}
											]
										}
									},
									{
										"SizeConstraintStatement": {
											"ComparisonOperator": "GT",
											"FieldToMatch": {
												"SingleHeader": {
													"Name": "cookie"
												}
											},
											"Size": 4093,
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "NONE"
												}
											]
										}
									},
									{
										"SizeConstraintStatement": {
											"ComparisonOperator": "GT",
											"FieldToMatch": {
												"Body": {
													"OversizeHandling": "CONTINUE"
												}
											},
											"Size": 4096,
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "NONE"
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericrestrictsizes",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-detect-blacklisted-ips",
						"Priority": 20,
						"Statement": {
							"IPSetReferenceStatement": {
								"Arn": {
									"Fn::GetAtt": [
										"ipgenericmatchblacklistedipsIPV4",
										"Arn"
									]
								}
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericblacklistedips",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-detect-bad-auth-tokens",
						"Priority": 30,
						"Statement": {
							"OrStatement": {
								"Statements": [
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"SingleHeader": {
													"Name": "authorization"
												}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LlRKVkE5NU9yTTdFMmNCYWIzMFJNSHJIRGNFZnhqb1laZ2VGT05GaDdIZ1E=",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"SingleHeader": {
													"Name": "cookie"
												}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "ZXhhbXBsZS1zZXNzaW9uLWlk",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericbadauthtokens",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-mitigate-sqli",
						"Priority": 40,
						"Statement": {
							"OrStatement": {
								"Statements": [
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"SingleHeader": {
													"Name": "cookie"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"SingleHeader": {
													"Name": "cookie"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"Body": {
													"OversizeHandling": "CONTINUE"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"SqliMatchStatement": {
											"FieldToMatch": {
												"Body": {
													"OversizeHandling": "CONTINUE"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericmitigatesqli",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-mitigate-xss",
						"Priority": 50,
						"Statement": {
							"OrStatement": {
								"Statements": [
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"SingleHeader": {
													"Name": "cookie"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"SingleHeader": {
													"Name": "cookie"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"Body": {
													"OversizeHandling": "CONTINUE"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"XssMatchStatement": {
											"FieldToMatch": {
												"Body": {
													"OversizeHandling": "CONTINUE"
												}
											},
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericmitigatexss",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-detect-rfi-lfi-traversal",
						"Priority": 60,
						"Statement": {
							"OrStatement": {
								"Statements": [
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Oi8v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Oi8v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Li4v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Li4v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Oi8v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Li4v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"QueryString": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Oi8v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "CONTAINS",
											"SearchStringBase64": "Li4v",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "HTML_ENTITY_DECODE"
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericdetectrfilfi",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-detect-php-insecure",
						"Priority": 70,
						"Statement": {
							"AndStatement": {
								"Statements": [
									{
										"OrStatement": {
											"Statements": [
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "c2FmZV9tb2RlPQ==",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "YWxsb3dfdXJsX2luY2x1ZGU9",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "X0VOVls=",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "ZGlzYWJsZV9mdW5jdGlvbnM9",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "X1NFUlZFUls=",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "YXV0b19wcmVwZW5kX2ZpbGU9",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "YXV0b19hcHBlbmRfZmlsZT0=",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"QueryString": {}
														},
														"PositionalConstraint": "CONTAINS",
														"SearchStringBase64": "b3Blbl9iYXNlZGlyPQ==",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												}
											]
										}
									},
									{
										"OrStatement": {
											"Statements": [
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"UriPath": {}
														},
														"PositionalConstraint": "ENDS_WITH",
														"SearchStringBase64": "cGhw",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												},
												{
													"ByteMatchStatement": {
														"FieldToMatch": {
															"UriPath": {}
														},
														"PositionalConstraint": "ENDS_WITH",
														"SearchStringBase64": "Lw==",
														"TextTransformations": [
															{
																"Priority": 0,
																"Type": "URL_DECODE"
															}
														]
													}
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericdetectphpinsecure",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-enforce-csrf",
						"Priority": 80,
						"Statement": {
							"AndStatement": {
								"Statements": [
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"Method": {}
											},
											"PositionalConstraint": "EXACTLY",
											"SearchStringBase64": "cG9zdA==",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									},
									{
										"NotStatement": {
											"Statement": {
												"SizeConstraintStatement": {
													"ComparisonOperator": "EQ",
													"FieldToMatch": {
														"SingleHeader": {
															"Name": "x-csrf-token"
														}
													},
													"Size": 36,
													"TextTransformations": [
														{
															"Priority": 0,
															"Type": "NONE"
														}
													]
												}
											}
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericenforcecsrf",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-detect-ssi",
						"Priority": 90,
						"Statement": {
							"OrStatement": {
								"Statements": [
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "STARTS_WITH",
											"SearchStringBase64": "L2luY2x1ZGVz",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LmJhY2t1cA==",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LmNmZw==",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LmNvbmY=",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LmluaQ==",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LmNvbmZpZw==",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LmJhaw==",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "ENDS_WITH",
											"SearchStringBase64": "LmxvZw==",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "LOWERCASE"
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericdetectssi",
							"SampledRequestsEnabled": true
						}
					},
					{
						"Action": {
							"Block": {}
						},
						"Name": "generic-detect-admin-access",
						"Priority": 100,
						"Statement": {
							"AndStatement": {
								"Statements": [
									{
										"NotStatement": {
											"Statement": {
												"IPSetReferenceStatement": {
													"Arn": {
														"Fn::GetAtt": [
															"ipgenericmatchadminremoteipIPV4",
															"Arn"
														]
													}
												}
											}
										}
									},
									{
										"ByteMatchStatement": {
											"FieldToMatch": {
												"UriPath": {}
											},
											"PositionalConstraint": "STARTS_WITH",
											"SearchStringBase64": "L2FkbWlu",
											"TextTransformations": [
												{
													"Priority": 0,
													"Type": "URL_DECODE"
												}
											]
										}
									}
								]
							}
						},
						"VisibilityConfig": {
							"CloudWatchMetricsEnabled": true,
							"MetricName": "genericdetectadminaccess",
							"SampledRequestsEnabled": true
						}
					}
				]
			},
			"DependsOn": [
				"ipgenericmatchadminremoteipIPV4",
				"ipgenericmatchblacklistedipsIPV4"
			]
		},
		"ipgenericmatchblacklistedipsIPV4": {
			"Type": "AWS::WAFv2::IPSet",
			"Properties": {
				"Addresses": [
					"127.0.0.1/32",
					"10.0.0.0/8",
					"192.168.0.0/16",
					"169.254.0.0/16",
					"172.16.0.0/16"
				],
				"IPAddressVersion": "IPV4",
				"Scope": "REGIONAL",
				"Description": "This is the WAF v2 IPSet migrated from v1 IPSet genericmatchblacklistedips ID 03aa90a38d6c4451848f3a86b6af93be",
				"Name": "generic-match-blacklisted-ips_migrated_446d02e1-9018-42d3-a189-ab7953f4c63c"
			}
		},
		"ipgenericmatchadminremoteipIPV4": {
			"Type": "AWS::WAFv2::IPSet",
			"Properties": {
				"Addresses": [
					"127.0.0.1/32"
				],
				"IPAddressVersion": "IPV4",
				"Scope": "REGIONAL",
				"Description": "This is the WAF v2 IPSet migrated from v1 IPSet genericmatchadminremoteip ID d6000ce6da634411814579bc121ba4cf",
				"Name": "generic-match-admin-remote-ip_migrated_6be41a50-e5e5-4f86-923a-0955ef8182d7"
			}
		}
	}
}

2. AWS WAF v2 ACLの作成

本番環境であれば事前JSONの内容をチェックすることをお勧めしますが、今回は「Create CloudFormation stack」のボタンをクリックして続けてAWS WAF v2 ACLを作成していきます。

ボタンをクリックした後は通常のスタック作成ウィザードが開始されるのでそのまま作成してきます。
パラメーター無しのテンプレートなので作業自体は難しく無いはずです。

aws-waf-classic-v1-eos-and-migration-11

aws-waf-classic-v1-eos-and-migration-12

最終的にエラー無くスタック作成が完了すればOKです。

aws-waf-classic-v1-eos-and-migration-13

aws-waf-classic-v1-eos-and-migration-14

作成されたリソースを見ると、

  • 各ルールはACL内の個別ルールに移行される
  • IPアドレスはIP Setへ移行される

という形になっていました。
既存のルールグループやRegexパターンセットに影響は出ない様なのでリソースの見通しはしやすいと思います。

AWS WAF v2のACLを確認するとちゃんと生成されており、

aws-waf-classic-v1-eos-and-migration-15

ルールの移行もできていました。

aws-waf-classic-v1-eos-and-migration-16

IPアドレス設定はこんな感じです。

aws-waf-classic-v1-eos-and-migration-17

そして制限事項で説明したとおり、保護対象との紐づけはありません。

aws-waf-classic-v1-eos-and-migration-18

本記事ではこれ以上の作業はしませんが、本番環境ではログ設定の追加やMarketplace製品のサブスクライブ等を続けて行う形になるでしょう。

3. 関連付け先の切り替え

最後にAWS WAF v2 ACLと保護対象を関連付けてやれば移行は完了です。

aws-waf-classic-v1-eos-and-migration-19

aws-waf-classic-v1-eos-and-migration-20

関連付けに際し明示的にClassic ACLの解除は不要で、AWS WAF v2 ACLに関連付けてやれば自動でClassic ACLとの関連付けは解除されます。

補足1 : 切り戻しについて

万が一移行後に問題が発生した場合はClassic ACLと関連付けしなおしてやれば切り戻しできます。

補足2 : AWS CLIを使う場合

AWS CLIから関連付けをする場合はaws wafv2 associate-web-aclコマンドで可能です。

# AWS WAF v2 ACLとの関連付け
aws wafv2 associate-web-acl \
    --web-acl-arn arn:aws:wafv2:ap-northeast-1:xxxxxxxxxxxxx:regional/webacl/generic-owasp-acl928a69a5-e49b-4237-9d23-23793211898a/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy \
    --resource-arn arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxxx:loadbalancer/app/test-alb/xxxxxxxxxxxxxxx

切り戻しの場合はAWS WAF Classicのaws waf-regional associate-web-aclコマンドを使います(ACLの場合)。

# 切り戻し : ALBの場合
aws waf-regional associate-web-acl \
    --web-acl-id zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz \
    --resource-arn arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxxx:loadbalancer/app/test-alb/xxxxxxxxxxxxxxx

最後に

以上となります。

今回はサンプルとして古いOWASP Top 10ルールを含む環境の移行を試してみましたが、この様な環境であれば単純移行ではなくAWSマネージドなルールグループを使う様に再設計すべきですね。
この他にも仕組みの違い等があるため、多くの場合で移行ツールを使って終わりということは無く設計の見直しも必要になることでしょう。

サービス終了まで約一年の猶予期間はありますが余裕を持って計画を進める様にしてください。

脚注
  1. 一応擁護しておくと、この制限はAWS WAF ClassicとAWS WAF v2の機能差や仕様の違いによるものでやむを得ない制限だと思います ↩︎

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.